import type { RuntimeBullet, DanmakuConfig, DanmakuItem } from './types';
import { CanvasRenderer } from './renderer/canvasRenderer';
import { TrackAllocator } from './trackAllocator';
import { measureTextWidth, getMeasureCacheSize } from './utils/measureText';
import { defaultPalette } from './theme';

export interface SchedulerOptions {
  canvas: HTMLCanvasElement;
  width: number;
  height: number;
  config: DanmakuConfig;
}

export class Scheduler {
  private config: DanmakuConfig;
  private renderer: CanvasRenderer;
  private allocator: TrackAllocator;
  private width: number;
  private height: number;
  private inFlight: RuntimeBullet[] = [];
  private queue: DanmakuItem[] = [];
  private dropped = 0;
  private lastTs = 0;
  private rafId: number | null = null;

  constructor(opts: SchedulerOptions) {
    this.config = opts.config;
    this.renderer = new CanvasRenderer({ canvas: opts.canvas, config: this.config });
    this.width = opts.width;
    this.height = opts.height;
    const lineHeight = this.config.fontSize * 1.4;
    this.allocator = new TrackAllocator(this.height, this.config.tracks, lineHeight);
    this.renderer.resize(this.width, this.height);
  }

  resize(width: number, height: number) {
    this.width = width;
    this.height = height;
    this.renderer.updateDPR();
    this.renderer.resize(width, height);
    const lineHeight = this.config.fontSize * 1.4;
    this.allocator.resize(this.height, this.config.tracks, lineHeight);
  }

  setConfig(partial: Partial<DanmakuConfig>) {
    this.config = { ...this.config, ...partial };
  }

  enqueue(item: Omit<DanmakuItem, 'id'> & { id?: string }) {
    const id = item.id ?? String(Date.now() + Math.random());
    this.queue.push({ ...item, id });
  }

  clear() {
    this.queue.length = 0;
    this.inFlight.length = 0;
    this.renderer.clear();
  }

  private trySpawn(nowMs: number) {
    if (this.inFlight.length >= this.config.maxConcurrent) return;
    if (this.queue.length === 0) return;
    const next = this.queue.shift()!;
    const fontSize = next.fontSize ?? this.config.fontSize;
    const font = `${fontSize}px ${this.config.fontFamily}`;
    const width = measureTextWidth(next.text, font);
    const isBottom = next.mode === 'bottom';
    const speed = isBottom ? 0 : clamp(
      next.speedPxPerSec ?? randomBetween(this.config.speedRange[0], this.config.speedRange[1]),
      this.config.speedRange[0],
      this.config.speedRange[1]
    );
    const color = next.color ?? defaultPalette[Math.floor(Math.random() * defaultPalette.length)];
    const alloc = isBottom ? { trackIndex: Math.max(0, this.config.tracks - 1), y: this.height - this.config.fontSize * 1.2 } : this.allocator.allocate(width, speed, this.width);
    if (!alloc) {
      // 轨道都不可用，回队尾或丢弃
      if (this.queue.length < this.config.maxConcurrent * 2) {
        this.queue.push(next);
      } else {
        this.dropped++;
      }
      return;
    }
    const bullet: RuntimeBullet = {
      id: next.id,
      text: next.text,
      color,
      fontSize,
      width,
      trackIndex: alloc.trackIndex,
      x: this.width,
      y: alloc.y,
      speedPxPerSec: speed,
      bornAtMs: nowMs,
      mode: next.mode,
      ttlMs: isBottom ? 3000 : undefined,
    };
    this.inFlight.push(bullet);
    if (!isBottom) this.allocator.setLastBullet(alloc.trackIndex, bullet);
  }

  private step(nowMs: number) {
    const dt = this.lastTs ? (nowMs - this.lastTs) / 1000 : 0;
    this.lastTs = nowMs;
    // 推进坐标
    if (dt > 0) {
      for (const b of this.inFlight) {
        if (b.mode === 'bottom') continue;
        b.x -= b.speedPxPerSec * dt;
      }
    }
    // 回收
    let i = 0;
    while (i < this.inFlight.length) {
      const b = this.inFlight[i];
      if (b.mode === 'bottom') {
        if (b.ttlMs && nowMs - b.bornAtMs >= b.ttlMs) {
          this.inFlight.splice(i, 1);
          continue;
        }
      } else if (b.x + b.width < 0) {
        this.inFlight.splice(i, 1);
      } else {
        i++;
      }
    }
    // 产出
    this.trySpawn(nowMs);
    // 绘制
    this.renderer.clear();
    this.renderer.draw(this.inFlight);
  }

  start() {
    if (this.rafId != null) return;
    const loop = (t: number) => {
      this.step(t);
      this.rafId = requestAnimationFrame(loop);
    };
    this.rafId = requestAnimationFrame(loop);
  }

  stop() {
    if (this.rafId != null) {
      cancelAnimationFrame(this.rafId);
      this.rafId = null;
    }
  }

  getMetrics() {
    return {
      droppedCount: this.dropped,
      inFlightCount: this.inFlight.length,
      measureCacheSize: getMeasureCacheSize(),
    };
  }
}

function randomBetween(min: number, max: number): number {
  return min + Math.random() * (max - min);
}

function clamp(v: number, min: number, max: number): number {
  return Math.max(min, Math.min(max, v));
}


